using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using WalletWasabi.Fluent.Helpers;
using WalletWasabi.Fluent.MathNet;

namespace WalletWasabi.Fluent.Controls;

public partial class LineChart : Control
{
	private readonly Dictionary<INotifyCollectionChanged, IDisposable> _collectionChangedSubscriptions;

	public LineChart()
	{
		_collectionChangedSubscriptions = new Dictionary<INotifyCollectionChanged, IDisposable>();

		AddHandler(PointerPressedEvent, PointerPressedHandler, RoutingStrategies.Tunnel);
		AddHandler(PointerReleasedEvent, PointerReleasedHandler, RoutingStrategies.Tunnel);
		AddHandler(PointerMovedEvent, PointerMovedHandler, RoutingStrategies.Tunnel);
		AddHandler(PointerExitedEvent, PointerLeaveHandler, RoutingStrategies.Tunnel);
	}

	private static double Clamp(double val, double min, double max)
	{
		return Math.Min(Math.Max(val, min), max);
	}

	private static Geometry CreateFillGeometry(IReadOnlyList<Point> points, double width, double height)
	{
		var geometry = new StreamGeometry();
		using var context = geometry.Open();
		context.BeginFigure(points[0], true);
		for (var i = 1; i < points.Count; i++)
		{
			context.LineTo(points[i]);
		}

		context.LineTo(new Point(width, height));
		context.LineTo(new Point(points[0].X, height));
		context.EndFigure(true);
		return geometry;
	}

	private static Geometry CreateStrokeGeometry(IReadOnlyList<Point> points)
	{
		var geometry = new StreamGeometry();
		using var context = geometry.Open();
		context.BeginFigure(points[0], false);
		for (var i = 1; i < points.Count; i++)
		{
			context.LineTo(points[i]);
		}

		context.EndFigure(false);
		return geometry;
	}

	private static FormattedText CreateFormattedText(string text, Typeface typeface, TextAlignment alignment, double fontSize)
	{
		var ft = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, fontSize, null)
		{
			TextAlignment = alignment,
			Trimming = TextTrimming.None
		};

		return ft;
	}

	private void UpdateXAxisCursorPosition(double x)
	{
		var xAxisValues = XAxisValues;
		if (xAxisValues is null || xAxisValues.Count == 0)
		{
			XAxisCurrentValue = double.NaN;
			return;
		}

		var areaWidth = Bounds.Width - AreaMargin.Left - AreaMargin.Right;
		var value = Clamp(x - AreaMargin.Left, 0, areaWidth);
		var factor = value / areaWidth;
		var index = (int)((xAxisValues.Count - 1) * factor);
		var currentValue = xAxisValues[index];
		XAxisCurrentValue = currentValue;
	}

	private Rect? GetXAxisCursorHitTestRect()
	{
		var chartWidth = Bounds.Width;
		var chartHeight = Bounds.Height;
		var areaMargin = AreaMargin;
		var areaWidth = chartWidth - areaMargin.Left - areaMargin.Right;
		var areaHeight = chartHeight - areaMargin.Top - areaMargin.Bottom;
		var areaRect = new Rect(areaMargin.Left, areaMargin.Top, areaWidth, areaHeight);
		var cursorPosition = GetCursorPosition(areaWidth);
		if (double.IsNaN(cursorPosition))
		{
			return null;
		}

		var cursorHitTestSize = 5;
		var cursorStrokeThickness = CursorStrokeThickness;
		var cursorHitTestRect = new Rect(
			areaMargin.Left + cursorPosition - cursorHitTestSize + (cursorStrokeThickness / 2),
			areaRect.Top,
			cursorHitTestSize + cursorHitTestSize,
			areaRect.Height);
		return cursorHitTestRect;
	}

	private void PointerLeaveHandler(object? sender, PointerEventArgs e)
	{
		Cursor = new Cursor(StandardCursorType.Arrow);
	}

	private void PointerMovedHandler(object? sender, PointerEventArgs e)
	{
		if (!EnableCursor)
		{
			return;
		}

		var position = e.GetPosition(this);
		if (_captured)
		{
			UpdateXAxisCursorPosition(position.X);
		}
		else
		{
			if (CursorStroke is null)
			{
				return;
			}

			var cursorHitTestRect = GetXAxisCursorHitTestRect();
			var cursorSizeWestEast = cursorHitTestRect is not null && cursorHitTestRect.Value.Contains(position);
			Cursor = cursorSizeWestEast
				? new Cursor(StandardCursorType.SizeWestEast)
				: new Cursor(StandardCursorType.Arrow);
		}
	}

	private void PointerReleasedHandler(object? sender, PointerReleasedEventArgs e)
	{
		if (!_captured)
		{
			return;
		}

		var position = e.GetPosition(this);
		var cursorHitTestRect = GetXAxisCursorHitTestRect();
		var cursorSizeWestEast = cursorHitTestRect is not null && cursorHitTestRect.Value.Contains(position);
		if (!cursorSizeWestEast)
		{
			Cursor = new Cursor(StandardCursorType.Arrow);
		}

		_captured = false;
	}

	private void PointerPressedHandler(object? sender, PointerPressedEventArgs e)
	{
		if (!EnableCursor)
		{
			return;
		}

		var position = e.GetPosition(this);
		UpdateXAxisCursorPosition(position.X);
		Cursor = new Cursor(StandardCursorType.SizeWestEast);
		_captured = true;
	}

	private LineChartState CreateChartState(double width, double height)
	{
		var state = new LineChartState
		{
			ChartWidth = width,
			ChartHeight = height,
			AreaMargin = AreaMargin
		};

		state.AreaWidth = width - state.AreaMargin.Left - state.AreaMargin.Right;
		state.AreaHeight = height - state.AreaMargin.Top - state.AreaMargin.Bottom;

		SetStateAreaPoints(state);

		SetStateXAxisLabels(state);
		SetStateYAxisLabels(state);

		SetStateXAxisCursor(state);

		return state;
	}

	private static IEnumerable<(double x, double y)> SplineInterpolate(double[] xs, double[] ys)
	{
		const int Divisions = 256;

		if (xs.Length > 2)
		{
			var spline = CubicSpline.InterpolatePchipSorted(xs, ys);

			for (var i = 0; i < xs.Length - 1; i++)
			{
				var a = xs[i];
				var b = xs[i + 1];
				var range = b - a;
				var step = range / Divisions;

				var t0 = xs[i];

				var xt0 = spline.Interpolate(xs[i]);

				yield return (t0, xt0);

				for (var t = a + step; t < b; t += step)
				{
					var xt = spline.Interpolate(t);

					yield return (t, xt);
				}
			}

			var tn = xs[^1];
			var xtn = spline.Interpolate(xs[^1]);

			yield return (tn, xtn);
		}
		else
		{
			for (var i = 0; i < xs.Length; i++)
			{
				yield return (xs[i], ys[i]);
			}
		}
	}

	private void SetStateAreaPoints(LineChartState state)
	{
		var xAxisValues = XAxisValues;
		var yAxisValues = YAxisValues;

		if (xAxisValues is null || xAxisValues.Count <= 1 || yAxisValues is null || yAxisValues.Count <= 1)
		{
			state.XAxisLabelStep = double.NaN;
			state.YAxisLabelStep = double.NaN;
			state.Points = null;
			return;
		}

		var logarithmicScale = YAxisLogarithmicScale;

		var yAxisValuesLogScaled = logarithmicScale
			? yAxisValues.Select(y => Math.Log(y)).ToList()
			: yAxisValues.ToList();

		var yAxisValuesLogScaledMax = yAxisValuesLogScaled.Max();

		var yAxisScaler = new StraightLineFormula();
		yAxisScaler.CalculateFrom(yAxisValuesLogScaledMax, 0, 0, state.AreaHeight);

		var yAxisValuesScaled = yAxisValuesLogScaled
			.Select(y => yAxisScaler.GetYforX(y))
			.ToList();

		var xAxisValuesEnumerable = xAxisValues as IEnumerable<double>;

		switch (XAxisPlotMode)
		{
			case AxisPlotMode.Normal:
				var min = XAxisMinimum ?? xAxisValues.Min();
				var max = xAxisValues.Max();

				var xAxisScaler = new StraightLineFormula();
				xAxisScaler.CalculateFrom(min, max, 0, state.AreaWidth);

				xAxisValuesEnumerable = xAxisValuesEnumerable.Select(x => xAxisScaler.GetYforX(x));
				break;

			case AxisPlotMode.EvenlySpaced:
				var pointStep = state.AreaWidth / (xAxisValues.Count - 1);

				xAxisValuesEnumerable = Enumerable.Range(0, xAxisValues.Count).Select(x => pointStep * x);
				break;

			case AxisPlotMode.Logarithmic:
				break;
		}

		if (SmoothCurve)
		{
			var interpolated = SplineInterpolate(xAxisValuesEnumerable.ToArray(), yAxisValuesScaled.ToArray());

			state.Points = interpolated.Select(pt => new Point(pt.x, pt.y)).ToArray();
		}
		else
		{
			state.Points = new Point[xAxisValues.Count];

			using var enumerator = xAxisValuesEnumerable.GetEnumerator();
			for (var i = 0; i < yAxisValuesScaled.Count; i++)
			{
				enumerator.MoveNext();
				state.Points[i] = new Point(enumerator.Current, yAxisValuesScaled[i]);
			}
		}
	}

	private void SetStateXAxisLabels(LineChartState state)
	{
		var xAxisLabels = XAxisLabels;

		if (xAxisLabels is not null)
		{
			state.XAxisLabelStep = xAxisLabels.Count <= 1
				? double.NaN
				: state.AreaWidth / (xAxisLabels.Count - 1);

			state.XAxisLabels = xAxisLabels.ToList();
		}
		else
		{
			AutoGenerateXAxisLabels(state);
		}
	}

	private void SetStateYAxisLabels(LineChartState state)
	{
		var yAxisLabels = YAxisLabels;

		if (yAxisLabels is not null)
		{
			state.YAxisLabelStep = yAxisLabels.Count <= 1
				? double.NaN
				: state.AreaHeight / (yAxisLabels.Count - 1);

			state.YAxisLabels = yAxisLabels.ToList();
		}
		else
		{
			AutoGenerateYAxisLabels(state);
		}
	}

	private void AutoGenerateXAxisLabels(LineChartState state)
	{
		var xAxisValues = XAxisValues;

		state.XAxisLabelStep = xAxisValues is null || xAxisValues.Count <= 1
			? double.NaN
			: state.AreaWidth / (xAxisValues.Count - 1);

		if (XAxisStroke is not null && XAxisValues is not null)
		{
			state.XAxisLabels = XAxisValues.Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList();
		}
	}

	private void AutoGenerateYAxisLabels(LineChartState state)
	{
		var yAxisValues = YAxisValues;

		state.YAxisLabelStep = yAxisValues is null || yAxisValues.Count <= 1
			? double.NaN
			: state.AreaHeight / (yAxisValues.Count - 1);

		if (YAxisStroke is not null && YAxisValues is not null)
		{
			state.YAxisLabels = YAxisValues.Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList();
		}
	}

	private double GetCursorPosition(double areaWidth)
	{
		var xAxisCurrentValue = XAxisCurrentValue;
		var xAxisValues = XAxisValues;
		if (double.IsNaN(xAxisCurrentValue) || xAxisValues is null || xAxisValues.Count == 0)
		{
			return double.NaN;
		}

		for (var i = 0; i < xAxisValues.Count; i++)
		{
			if (xAxisValues[i] <= xAxisCurrentValue)
			{
				var cursorPosition = i == 0 ? 0.0 : areaWidth / (xAxisValues.Count - 1) * i;
				return cursorPosition;
			}
		}

		return double.NaN;
	}

	private void SetStateXAxisCursor(LineChartState state)
	{
		state.XAxisCursorPosition = GetCursorPosition(state.AreaWidth);
	}

	private void DrawAreaFill(DrawingContext context, LineChartState state)
	{
		var brush = AreaFill;
		if (brush is null
			|| state.Points is null
			|| state.AreaWidth <= 0
			|| state.AreaHeight <= 0
			|| state.AreaWidth < AreaMinViableWidth
			|| state.AreaHeight < AreaMinViableHeight)
		{
			return;
		}

		var deflate = 0.5;
		var geometry = CreateFillGeometry(state.Points, state.AreaWidth, state.AreaHeight);
		var transform = context.PushTransform(
			Matrix.CreateTranslation(
				state.AreaMargin.Left + deflate,
				state.AreaMargin.Top + deflate));
		context.DrawGeometry(brush, null, geometry);
		transform.Dispose();
	}

	private void DrawAreaStroke(DrawingContext context, LineChartState state)
	{
		var brush = AreaStroke;
		if (brush is null
			|| state.Points is null
			|| state.AreaWidth <= 0
			|| state.AreaHeight <= 0
			|| state.AreaWidth < AreaMinViableWidth
			|| state.AreaHeight < AreaMinViableHeight)
		{
			return;
		}

		var thickness = AreaStrokeThickness;
		var dashStyle = AreaStrokeDashStyle;
		var lineCap = AreaStrokeLineCap;
		var lineJoin = AreaStrokeLineJoin;
		var miterLimit = AreaStrokeMiterLimit;
		var pen = new Pen(brush, thickness, dashStyle, lineCap, lineJoin, miterLimit);
		var deflate = thickness * 0.5;
		var geometry = CreateStrokeGeometry(state.Points);
		var transform = context.PushTransform(
			Matrix.CreateTranslation(
				state.AreaMargin.Left + deflate,
				state.AreaMargin.Top + deflate));
		context.DrawGeometry(null, pen, geometry);
		transform.Dispose();
	}

	private void DrawCursor(DrawingContext context, LineChartState state)
	{
		var brush = CursorStroke;
		if (brush is null
			|| double.IsNaN(state.XAxisCursorPosition)
			|| state.AreaWidth <= 0
			|| state.AreaHeight <= 0
			|| state.AreaWidth < AreaMinViableWidth
			|| state.AreaHeight < AreaMinViableHeight)
		{
			return;
		}

		var thickness = CursorStrokeThickness;
		var dashStyle = CursorStrokeDashStyle;
		var lineCap = CursorStrokeLineCap;
		var lineJoin = CursorStrokeLineJoin;
		var miterLimit = CursorStrokeMiterLimit;
		var pen = new Pen(brush, thickness, dashStyle, lineCap, lineJoin, miterLimit);
		var deflate = thickness * 0.5;
		var p1 = new Point(state.XAxisCursorPosition + deflate, 0);
		var p2 = new Point(state.XAxisCursorPosition + deflate, state.AreaHeight);
		var transform = context.PushTransform(
			Matrix.CreateTranslation(
				state.AreaMargin.Left,
				state.AreaMargin.Top));
		context.DrawLine(pen, p1, p2);
		transform.Dispose();
	}

	private void DrawXAxis(DrawingContext context, LineChartState state)
	{
		var brush = XAxisStroke;
		if (brush is null
			|| state.AreaWidth <= 0
			|| state.AreaHeight <= 0
			|| state.AreaWidth < XAxisMinViableWidth
			|| state.AreaHeight < XAxisMinViableHeight)
		{
			return;
		}

		var size = XAxisArrowSize;
		var opacity = XAxisOpacity;
		var thickness = XAxisStrokeThickness;
		var pen = new Pen(brush, thickness, null, PenLineCap.Round);
		var deflate = thickness * 0.5;
		var offset = XAxisOffset;
		var p1 = new Point(
			state.AreaMargin.Left + offset.X,
			state.AreaMargin.Top + state.AreaHeight + offset.Y + deflate);
		var p2 = new Point(
			state.AreaMargin.Left + state.AreaWidth,
			state.AreaMargin.Top + state.AreaHeight + offset.Y + deflate);
		var opacityState = context.PushOpacity(opacity);
		context.DrawLine(pen, p1, p2);
		var p3 = new Point(p2.X, p2.Y);
		var p4 = new Point(p2.X - size, p2.Y - size);
		context.DrawLine(pen, p3, p4);
		var p5 = new Point(p2.X, p2.Y);
		var p6 = new Point(p2.X - size, p2.Y + size);
		context.DrawLine(pen, p5, p6);
		opacityState.Dispose();
	}

	private static Point AlignXAxisLabelOffset(Point offsetCenter, double width, int index, int count, LabelAlignment alignment)
	{
		var isFirst = index == 0;
		var isLast = index == count - 1;

		return alignment switch
		{
			LabelAlignment.Auto => isFirst
				? offsetCenter.WithX(offsetCenter.X - (width / 2))
				: isLast
					? offsetCenter.WithX(offsetCenter.X - width - (width / 2))
					: offsetCenter.WithX(offsetCenter.X - width),
			LabelAlignment.Left => offsetCenter.WithX(offsetCenter.X - width - (width / 2)),
			LabelAlignment.Center => offsetCenter.WithX(offsetCenter.X - width),
			LabelAlignment.Right => offsetCenter.WithX(offsetCenter.X - (width / 2)),
			_ => offsetCenter.WithX(offsetCenter.X - width)
		};
	}

	private void DrawXAxisLabels(DrawingContext context, LineChartState state)
	{
		var foreground = XAxisLabelForeground;
		if (foreground is null
			|| state.XAxisLabels is null
			|| double.IsNaN(state.XAxisLabelStep)
			|| state.ChartWidth <= 0
			|| state.ChartHeight <= 0
			|| state.ChartHeight - state.AreaMargin.Top < state.AreaMargin.Bottom)
		{
			return;
		}

		var opacity = XAxisLabelOpacity;
		var fontFamily = XAxisLabelFontFamily;
		var fontStyle = XAxisLabelFontStyle;
		var fontWeight = XAxisLabelFontWeight;
		var typeface = new Typeface(fontFamily, fontStyle, fontWeight);
		var fontSize = XAxisLabelFontSize;
		var offset = XAxisLabelOffset;
		var angleRadians = Math.PI / 180.0 * XAxisLabelAngle;
		var alignment = XAxisLabelAlignment;
		var originTop = state.AreaMargin.Top + state.AreaHeight;
		var formattedTextLabels = new List<FormattedText>();
		var constrainWidthMax = 0.0;
		var constrainHeightMax = 0.0;
		var labels = state.XAxisLabels;

		for (var i = 0; i < labels.Count; i++)
		{
			var label = labels[i];
			var formattedText = CreateFormattedText(label, typeface, TextAlignment.Left, fontSize);
			formattedTextLabels.Add(formattedText);
			constrainWidthMax = Math.Max(constrainWidthMax, formattedText.Width);
			constrainHeightMax = Math.Max(constrainHeightMax, formattedText.Height);
		}

		var constraintMax = new Size(constrainWidthMax, constrainHeightMax);
		var offsetTransform = context.PushTransform(Matrix.CreateTranslation(offset.X, offset.Y));

		for (var i = 0; i < formattedTextLabels.Count; i++)
		{
			formattedTextLabels[i].MaxTextHeight = constraintMax.Height;
			formattedTextLabels[i].MaxTextWidth = constraintMax.Width;
			var origin = new Point((i * state.XAxisLabelStep) + (constraintMax.Width / 2) + state.AreaMargin.Left, originTop);
			var offsetCenter = new Point((constraintMax.Width / 2) - (constraintMax.Width / 2), 0);
			offsetCenter = AlignXAxisLabelOffset(
				offsetCenter,
				formattedTextLabels[i].Width,
				i,
				formattedTextLabels.Count,
				alignment);
			var xPosition = origin.X + (constraintMax.Width / 2);
			var yPosition = origin.Y + (constraintMax.Height / 2);
			var matrix = Matrix.CreateTranslation(-xPosition, -yPosition)
						 * Matrix.CreateRotation(angleRadians)
						 * Matrix.CreateTranslation(xPosition, yPosition);
			var labelTransform = context.PushTransform(matrix);
			var opacityState = context.PushOpacity(opacity);
			formattedTextLabels[i].SetForegroundBrush(foreground);
			context.DrawText(formattedTextLabels[i], origin + offsetCenter);
			opacityState.Dispose();
			labelTransform.Dispose();
		}

		offsetTransform.Dispose();
	}

	private void DrawXAxisTitle(DrawingContext context, LineChartState state)
	{
		var foreground = XAxisTitleForeground;
		if (foreground is null)
		{
			return;
		}

		if (state.AreaWidth <= 0
			|| state.AreaHeight <= 0
			|| state.AreaWidth < XAxisMinViableWidth
			|| state.AreaHeight < XAxisMinViableHeight)
		{
			return;
		}

		var opacity = XAxisTitleOpacity;
		var fontFamily = XAxisTitleFontFamily;
		var fontStyle = XAxisTitleFontStyle;
		var fontWeight = XAxisTitleFontWeight;
		var typeface = new Typeface(fontFamily, fontStyle, fontWeight);
		var fontSize = XAxisTitleFontSize;
		var offset = XAxisTitleOffset;
		var size = new Size(state.AreaWidth, XAxisTitleSize.Height);
		var angleRadians = Math.PI / 180.0 * XAxisTitleAngle;
		var alignment = XAxisTitleAlignment;
		var offsetTransform = context.PushTransform(Matrix.CreateTranslation(offset.X, offset.Y));
		var origin = new Point(state.AreaMargin.Left, state.AreaHeight + state.AreaMargin.Bottom);
		var formattedText = CreateFormattedText(XAxisTitle, typeface, alignment, fontSize);
		var xPosition = origin.X + (size.Width / 2);
		var yPosition = origin.Y + (size.Height / 2);

		var matrix = Matrix.CreateTranslation(-xPosition, -yPosition)
					 * Matrix.CreateRotation(angleRadians)
					 * Matrix.CreateTranslation(xPosition, yPosition);
		var labelTransform = context.PushTransform(matrix);
		var offsetCenter = new Point(0, 0);
		var opacityState = context.PushOpacity(opacity);
		formattedText.SetForegroundBrush(foreground);
		context.DrawText(formattedText, origin + offsetCenter);
		opacityState.Dispose();
		labelTransform.Dispose();
		offsetTransform.Dispose();
	}

	private void DrawYAxis(DrawingContext context, LineChartState state)
	{
		var brush = YAxisStroke;
		if (brush is null
			|| state.AreaWidth <= 0
			|| state.AreaHeight <= 0
			|| state.AreaWidth < YAxisMinViableWidth
			|| state.AreaHeight < YAxisMinViableHeight)
		{
			return;
		}

		var size = YAxisArrowSize;
		var opacity = YAxisOpacity;
		var thickness = YAxisStrokeThickness;
		var pen = new Pen(brush, thickness, null, PenLineCap.Round);
		var deflate = thickness * 0.5;
		var offset = YAxisOffset;
		var p1 = new Point(
			state.AreaMargin.Left + offset.X + deflate,
			state.AreaMargin.Top);
		var p2 = new Point(
			state.AreaMargin.Left + offset.X + deflate,
			state.AreaMargin.Top + state.AreaHeight + offset.Y);
		var opacityState = context.PushOpacity(opacity);
		context.DrawLine(pen, p1, p2);
		var p3 = new Point(p1.X, p1.Y);
		var p4 = new Point(p1.X - size, p1.Y + size);
		context.DrawLine(pen, p3, p4);
		var p5 = new Point(p1.X, p1.Y);
		var p6 = new Point(p1.X + size, p1.Y + size);
		context.DrawLine(pen, p5, p6);
		opacityState.Dispose();
	}

	private static TextAlignment GetYAxisLabelTextAlignment(LabelAlignment alignment)
	{
		return alignment switch
		{
			LabelAlignment.Auto => TextAlignment.Right,
			LabelAlignment.Left => TextAlignment.Left,
			LabelAlignment.Center => TextAlignment.Center,
			LabelAlignment.Right => TextAlignment.Right,
			_ => TextAlignment.Center
		};
	}

	private static Point AlignYAxisLabelOffset(Point offsetCenter, double height, int index, int count, LabelAlignment alignment)
	{
		var isFirst = index == 0;
		var isLast = index == count - 1;

		return alignment switch
		{
			LabelAlignment.Auto => isFirst
				? offsetCenter.WithY(offsetCenter.Y + (height / 2))
				: isLast
					? offsetCenter.WithY(offsetCenter.Y - height + (height / 2))
					: offsetCenter.WithY(offsetCenter.Y),
			LabelAlignment.Left => offsetCenter,
			LabelAlignment.Center => offsetCenter,
			LabelAlignment.Right => offsetCenter,
			_ => offsetCenter
		};
	}

	private void DrawYAxisLabels(DrawingContext context, LineChartState state)
	{
		var foreground = YAxisLabelForeground;
		if (foreground is null
			|| state.YAxisLabels is null
			|| double.IsNaN(state.YAxisLabelStep)
			|| state.ChartWidth <= 0
			|| state.ChartWidth - state.AreaMargin.Right < state.AreaMargin.Left
			|| state.ChartHeight <= 0)
		{
			return;
		}

		var opacity = YAxisLabelOpacity;
		var fontFamily = YAxisLabelFontFamily;
		var fontStyle = YAxisLabelFontStyle;
		var fontWeight = YAxisLabelFontWeight;
		var typeface = new Typeface(fontFamily, fontStyle, fontWeight);
		var fontSize = YAxisLabelFontSize;
		var offset = YAxisLabelOffset;
		var angleRadians = Math.PI / 180.0 * YAxisLabelAngle;
		var alignment = YAxisLabelAlignment;
		var originLeft = state.AreaMargin.Left;
		var formattedTextLabels = new List<FormattedText>();
		var constrainWidthMax = 0.0;
		var constrainHeightMax = 0.0;
		var labels = state.YAxisLabels;

		for (var i = labels.Count - 1; i >= 0; i--)
		{
			var label = labels[i];
			var textAlignment = GetYAxisLabelTextAlignment(alignment);
			var formattedText = CreateFormattedText(label, typeface, textAlignment, fontSize);
			formattedTextLabels.Add(formattedText);
			constrainWidthMax = Math.Max(constrainWidthMax, formattedText.Width);
			constrainHeightMax = Math.Max(constrainHeightMax, formattedText.Height);
		}

		var constraintMax = new Size(constrainWidthMax, constrainHeightMax);
		var offsetTransform = context.PushTransform(Matrix.CreateTranslation(offset.X, offset.Y));

		for (var i = 0; i < formattedTextLabels.Count; i++)
		{
			formattedTextLabels[i].MaxTextHeight = constraintMax.Height;
			formattedTextLabels[i].MaxTextWidth = constraintMax.Width;

			var origin = new Point(
				originLeft,
				(i * state.YAxisLabelStep) - (constraintMax.Height / 2) + state.AreaMargin.Top);
			var offsetCenter = new Point((constraintMax.Width / 2) - (constraintMax.Width / 2), 0);
			offsetCenter = AlignYAxisLabelOffset(
				offsetCenter,
				formattedTextLabels[i].Height,
				i,
				formattedTextLabels.Count,
				alignment);
			var xPosition = origin.X + (constraintMax.Width / 2);
			var yPosition = origin.Y + (constraintMax.Height / 2);
			var matrix = Matrix.CreateTranslation(-xPosition, -yPosition)
						 * Matrix.CreateRotation(angleRadians)
						 * Matrix.CreateTranslation(xPosition, yPosition);
			var labelTransform = context.PushTransform(matrix);
			var opacityState = context.PushOpacity(opacity);
			formattedTextLabels[i].SetForegroundBrush(foreground);
			context.DrawText(formattedTextLabels[i], origin + offsetCenter);
			opacityState.Dispose();
			labelTransform.Dispose();
		}

		offsetTransform.Dispose();
	}

	private void DrawYAxisTitle(DrawingContext context, LineChartState state)
	{
		var foreground = YAxisTitleForeground;
		if (foreground is null)
		{
			return;
		}

		if (state.AreaWidth <= 0
			|| state.AreaHeight <= 0
			|| state.AreaWidth < YAxisMinViableWidth
			|| state.AreaHeight < YAxisMinViableHeight)
		{
			return;
		}

		var opacity = YAxisTitleOpacity;
		var fontFamily = YAxisTitleFontFamily;
		var fontStyle = YAxisTitleFontStyle;
		var fontWeight = YAxisTitleFontWeight;
		var typeface = new Typeface(fontFamily, fontStyle, fontWeight);
		var fontSize = YAxisTitleFontSize;
		var offset = YAxisTitleOffset;
		var size = YAxisTitleSize;
		var angleRadians = Math.PI / 180.0 * YAxisTitleAngle;
		var alignment = YAxisTitleAlignment;
		var offsetTransform = context.PushTransform(Matrix.CreateTranslation(offset.X, offset.Y));
		var origin = new Point(state.AreaMargin.Left, state.AreaHeight + state.AreaMargin.Top);
		var formattedText = CreateFormattedText(YAxisTitle, typeface, alignment, fontSize);
		var xPosition = origin.X + (size.Width / 2);
		var yPosition = origin.Y + (size.Height / 2);
		var matrix = Matrix.CreateTranslation(-xPosition, -yPosition)
					 * Matrix.CreateRotation(angleRadians)
					 * Matrix.CreateTranslation(xPosition, yPosition);
		var labelTransform = context.PushTransform(matrix);
		var offsetCenter = new Point(0, (size.Height / 2) - (formattedText.Height / 2));
		var opacityState = context.PushOpacity(opacity);
		formattedText.SetForegroundBrush(foreground);
		context.DrawText(formattedText, origin + offsetCenter);
		opacityState.Dispose();
		labelTransform.Dispose();
		offsetTransform.Dispose();
	}

	private void DrawBorder(DrawingContext context, LineChartState state)
	{
		var brush = BorderBrush;
		if (brush is null || state.AreaWidth <= 0 || state.AreaHeight <= 0)
		{
			return;
		}

		var thickness = BorderThickness;
		var radiusX = BorderRadiusX;
		var radiusY = BorderRadiusY;
		var pen = new Pen(brush, thickness, null, PenLineCap.Round);
		var rect = new Rect(0, 0, state.ChartWidth, state.ChartHeight);
		var rectDeflate = rect.Deflate(thickness * 0.5);
		context.DrawRectangle(Brushes.Transparent, pen, rectDeflate, radiusX, radiusY);
	}

	private void UpdateSubscription(INotifyCollectionChanged? oldValue, INotifyCollectionChanged? newValue)
	{
		if (oldValue is { } && _collectionChangedSubscriptions.TryGetValue(oldValue, out IDisposable? value))
		{
			value.Dispose();
			_collectionChangedSubscriptions.Remove(oldValue);
		}

		if (newValue is { })
		{
			newValue.CollectionChanged += ItemsPropertyCollectionChanged;

			_collectionChangedSubscriptions[newValue] =
				Disposable.Create(() => newValue.CollectionChanged -= ItemsPropertyCollectionChanged);
		}
	}

	private void ItemsPropertyCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
	{
		InvalidateVisual();
	}

	protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
	{
		base.OnPropertyChanged(change);

		if (change.Property == XAxisValuesProperty || change.Property == YAxisValuesProperty ||
			change.Property == XAxisLabelsProperty || change.Property == YAxisLabelsProperty)
		{
			var oldINCC = change.OldValue as INotifyCollectionChanged;
			var newINCC = change.NewValue as INotifyCollectionChanged;
			UpdateSubscription(oldINCC, newINCC);
		}
	}

	public override void Render(DrawingContext context)
	{
		base.Render(context);

		var state = CreateChartState(Bounds.Width, Bounds.Height);

		DrawAreaFill(context, state);
		DrawAreaStroke(context, state);

		if (EnableCursor)
		{
			DrawCursor(context, state);
		}

		DrawXAxis(context, state);
		DrawXAxisTitle(context, state);
		DrawXAxisLabels(context, state);

		DrawYAxis(context, state);
		DrawYAxisTitle(context, state);
		DrawYAxisLabels(context, state);

		DrawBorder(context, state);
	}

	private class LineChartState
	{
		public double ChartWidth { get; set; }
		public double ChartHeight { get; set; }
		public double AreaWidth { get; set; }
		public double AreaHeight { get; set; }
		public Thickness AreaMargin { get; set; }
		public Point[]? Points { get; set; }
		public List<string>? XAxisLabels { get; set; }
		public double XAxisLabelStep { get; set; }
		public List<string>? YAxisLabels { get; set; }
		public double YAxisLabelStep { get; set; }
		public double XAxisCursorPosition { get; set; }
	}
}
